﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Core;
using Tesla.Math;

namespace Tesla.Scene.Shape {
    /// <summary>
    /// Mesh that represents a sphere. Geometry is indexed with each vertex
    /// containg a Position, Normal, and Texture Coordinate by default.
    /// </summary>
    public class Sphere : Mesh {

        private float _radius;
        private Vector3 _center;
        private SphereTextureMode _textureMode;

        /// <summary>
        /// Gets the texture mode for the sphere.
        /// </summary>
        public SphereTextureMode TextureMode {
            get {
                return _textureMode;
            }
        }

        /// <summary>
        /// Gets the center of the sphere.
        /// </summary>
        public Vector3 Center {
            get {
                return _center;
            }
        }

        /// <summary>
        /// Gets the radius of the sphere.
        /// </summary>
        public float Radius {
            get {
                return _radius;
            }
        }

        /// <summary>
        /// Creates a new instance of a sphere centered at the origin.
        /// </summary>
        /// <param name="name">Name of the sphere</param>
        /// <param name="zSamples">Number of samples along the z-axis</param>
        /// <param name="radSamples">Number of samples along the radius</param>
        /// <param name="radius">Radius of the sphere</param>
        public Sphere(String name, int zSamples, int radSamples, float radius)
            : base(name) {
            SetData(new Vector3(0, 0, 0), zSamples, radSamples, radius);
        }

        /// <summary>
        /// Creates a new instance of a sphere.
        /// </summary>
        /// <param name="name">Name of the sphere</param>
        /// <param name="center">Center of the sphere</param>
        /// <param name="zSamples">Number of samples along the z-axis</param>
        /// <param name="radSamples">Number of samples along the radius</param>
        /// <param name="radius">Radius of the sphere</param>
        public Sphere(String name, Vector3 center, int zSamples, int radSamples, float radius)
            : base(name) {
            SetData(center, zSamples, radSamples, radius);
        }

        /// <summary>
        /// Creates a new instance of a sphere.
        /// </summary>
        /// <param name="name">Name of the sphere</param>
        /// <param name="center">Center of the sphere</param>
        /// <param name="zSamples">Number of samples along the z-axis</param>
        /// <param name="radSamples">Number of samples along the radius</param>
        /// <param name="radius">Radius of the sphere</param>
        /// <param name="textureMode">Texture mode to use</param>
        public Sphere(String name, Vector3 center, int zSamples, int radSamples, float radius, SphereTextureMode textureMode)
            : base(name) {
            SetData(center, zSamples, radSamples, radius, textureMode);
        }

        /// <summary>
        /// Sets the sphere's data and calls Mesh.Reconstruct()
        /// </summary>
        /// <param name="center">Center of the sphere</param>
        /// <param name="zSamples">Number of samples along the z-axis</param>
        /// <param name="radSamples">Number of samples along the radius</param>
        /// <param name="radius">Radius of the sphere</param>
        public void SetData(Vector3 center, int zSamples, int radSamples, float radius) {
            SetData(center, zSamples, radSamples, radius, SphereTextureMode.Linear);
        }

        /// <summary>
        /// Sets the sphere's data and calls Mesh.Reconstruct()
        /// </summary>
        /// <param name="center">Center of the sphere</param>
        /// <param name="zSamples">Number of samples along the z-axis</param>
        /// <param name="radSamples">Number of samples along the radius</param>
        /// <param name="radius">Radius of the sphere</param>
        /// <param name="textureMode">Texture mode to use</param>
        public void SetData(Vector3 center, int zSamples, int radSamples, float radius, SphereTextureMode textureMode) {
            _center = center;
            _radius = radius;
            _textureMode = textureMode;

            GenerateGeometry(zSamples, radSamples);
            GenerateIndices(zSamples, radSamples);
            this.MeshData.Reconstruct();
            UpdateModelBound();
        }

        private void GenerateGeometry(int zSamples, int radSamples) {
            int vertCount = (zSamples - 2) * (radSamples + 1) + 2;
            Vector3[] vertices = new Vector3[vertCount];
            Vector3[] normals = new Vector3[vertCount];
            Vector2[] texCoords = new Vector2[vertCount];

            // Generate points on the unit circle to be used in computing the mesh
            // points on a sphere slice.
            float fInvRs = 1.0f / radSamples;
            float fZFactor = 2.0f / (zSamples - 1);
            float[] afSin = new float[(radSamples + 1)];
            float[] afCos = new float[(radSamples + 1)];
            for(int iR = 0; iR < radSamples; iR++) {
                float fAngle = MathHelper.TwoPi * fInvRs * iR;
                afCos[iR] = (float) System.Math.Cos(fAngle);
                afSin[iR] =(float) System.Math.Sin(fAngle);
            }

            afSin[radSamples] = afSin[0];
            afCos[radSamples] = afCos[0];

            //Generate sphere 
            int index = 0;
            for(int iZ = 1; iZ < (zSamples - 1); iZ++) {
                float fAFraction = MathHelper.PiOver2 * (-1.0f + fZFactor * iZ); //in (-pi/2,pi/2)
                float fZFraction = (float) System.Math.Sin(fAFraction); //in (-1,1)
                float fZ = _radius * fZFraction;

                //Compute center of slice
                Vector3 kSliceCenter = _center;
                kSliceCenter.Z = kSliceCenter.Z + fZ;

                //Compute radius of slice
                float fSliceRadius = (float) System.Math.Sqrt(System.Math.Abs((_radius * _radius) - (fZ * fZ)));

                //Compute slice vertices with duplication at end point
                int iSave = index;
                for(int iR = 0; iR < radSamples; iR++) {
                    float fRadialFraction = iR * fInvRs; //in [0,1)
                    Vector3 kRadial = new Vector3(afCos[iR], afSin[iR], 0);
                    kRadial *= fSliceRadius;
                    vertices[index] = kSliceCenter + kRadial;
                    normals[index] = Vector3.Normalize(vertices[index] - _center);

                    if(_textureMode == SphereTextureMode.Linear) {
                        texCoords[index] = new Vector2(fRadialFraction, 1 - .5f * (fZFraction + 1.0f));
                    } else if(_textureMode == SphereTextureMode.Projected) {
                        texCoords[index] = new Vector2(fRadialFraction, 1 - (1.0f / MathHelper.Pi) * (MathHelper.PiOver2 + (float) System.Math.Asin(fZFraction)));
                    } else if(_textureMode == SphereTextureMode.Polar) {
                        float r = (MathHelper.PiOver2 - System.Math.Abs(fAFraction)) / MathHelper.Pi;
                        float u = r * afCos[iR] + .5f;
                        float v = r * afSin[iR] + .5f;
                        texCoords[index] = new Vector2(u, 1 - v);
                    }
                    index++;
                }
                vertices[index] = vertices[iSave];
                normals[index] = normals[iSave];

                if(_textureMode == SphereTextureMode.Linear) {
                    texCoords[index] = new Vector2(1.0f, 1 - .5f * (fZFraction + 1.0f));
                } else if(_textureMode == SphereTextureMode.Projected) {
                    texCoords[index] = new Vector2(1.0f, 1 - (1.0f / MathHelper.Pi) * (MathHelper.PiOver2 + (float) System.Math.Asin(fZFraction)));
                } else if(_textureMode == SphereTextureMode.Polar) {
                    float r = (MathHelper.PiOver2 - System.Math.Abs(fAFraction)) / MathHelper.Pi;
                    texCoords[index] = new Vector2(r + .5f, .5f);
                }
                index++;
            }

            //South pole
            vertices[index] = new Vector3(_center.X, _center.Y, _center.Z - _radius);
            normals[index] = new Vector3(0, 0, -1);
            if(_textureMode == SphereTextureMode.Polar) {
                texCoords[index] = new Vector2(.5f, .5f);
            } else {
                texCoords[index] = new Vector2(.5f, 1);
            }
            index++;
            //North pole
            vertices[index] = new Vector3(_center.X, _center.Y, _center.Z + _radius);
            normals[index] = new Vector3(0, 0, 1);
            if(_textureMode == SphereTextureMode.Polar) {
                texCoords[index] = new Vector2(.5f, .5f);
            } else {
                texCoords[index] = new Vector2(.5f, 0.0f);
            }

            this.MeshData.Positions = new DataBuffer<Vector3>(vertices);
            this.MeshData.Normals = new DataBuffer<Vector3>(normals);
            this.MeshData.TextureCoordinates = new DataBuffer<Vector2>(texCoords);
            this.MeshData.UpdateVertexCount();
        }

        private void GenerateIndices(int zSamples, int radSamples) {
            int triCount = 2 * (zSamples - 2) * radSamples;
            int[] indices = new int[3*triCount];

            int index = 0;
            for(int iZ = 0, iZStart = 0; iZ < (zSamples - 3); iZ++) {
                int i0 = iZStart;
                int i1 = i0 + 1;
                iZStart += (radSamples + 1);
                int i2 = iZStart;
                int i3 = i2 + 1;
                for(int i = 0; i < radSamples; i++, index += 6) {
                    indices[index] = i1;
                    indices[index+1] = i0++;
                    indices[index+2] = i2;

                    indices[index+3] = i3++;
                    indices[index+4] = i1++;
                    indices[index+5] = i2++;
                }
            }

            //South pole triangles
            for(int i = 0; i < radSamples; i++, index += 3) {
                indices[index] = i;
                indices[index+1] = i + 1;
                indices[index+2] = this.MeshData.VertexCount - 2;
            }

            //North pole triangles
            int iOffset = (zSamples - 3) * (radSamples + 1);
            for(int i = 0; i < radSamples; i++, index += 3) {
                indices[index] = i + iOffset;
                indices[index+1] = this.MeshData.VertexCount - 1;
                indices[index+2] = i + 1 + iOffset;
            }
            this.MeshData.Indices = new DataBuffer<int>(indices);
        }
    }
}
